// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/views/controls/menu/submenu_view.h"

#include <algorithm>

#include "base/compiler_specific.h"
#include "ui/accessibility/ax_view_state.h"
#include "ui/base/ime/input_method.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/safe_integer_conversions.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_host.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/menu_scroll_view_container.h"
#include "ui/views/widget/root_view.h"
#include "ui/views/widget/widget.h"

namespace {

// Height of the drop indicator. This should be an even number.
const int kDropIndicatorHeight = 2;

// Color of the drop indicator.
const SkColor kDropIndicatorColor = SK_ColorBLACK;

} // namespace

namespace views {

// static
const char SubmenuView::kViewClassName[] = "SubmenuView";

SubmenuView::SubmenuView(MenuItemView* parent)
    : parent_menu_item_(parent)
    , host_(NULL)
    , drop_item_(NULL)
    , drop_position_(MenuDelegate::DROP_NONE)
    , scroll_view_container_(NULL)
    , max_minor_text_width_(0)
    , minimum_preferred_width_(0)
    , resize_open_menu_(false)
    , scroll_animator_(new ScrollAnimator(this))
    , roundoff_error_(0)
    , prefix_selector_(this)
{
    DCHECK(parent);
    // We'll delete ourselves, otherwise the ScrollView would delete us on close.
    set_owned_by_client();
}

SubmenuView::~SubmenuView()
{
    // The menu may not have been closed yet (it will be hidden, but not
    // necessarily closed).
    Close();

    delete scroll_view_container_;
}

int SubmenuView::GetMenuItemCount()
{
    int count = 0;
    for (int i = 0; i < child_count(); ++i) {
        if (child_at(i)->id() == MenuItemView::kMenuItemViewID)
            count++;
    }
    return count;
}

MenuItemView* SubmenuView::GetMenuItemAt(int index)
{
    for (int i = 0, count = 0; i < child_count(); ++i) {
        if (child_at(i)->id() == MenuItemView::kMenuItemViewID && count++ == index) {
            return static_cast<MenuItemView*>(child_at(i));
        }
    }
    NOTREACHED();
    return NULL;
}

PrefixSelector* SubmenuView::GetPrefixSelector()
{
    return &prefix_selector_;
}

void SubmenuView::ChildPreferredSizeChanged(View* child)
{
    if (!resize_open_menu_)
        return;

    MenuItemView* item = GetMenuItem();
    MenuController* controller = item->GetMenuController();

    if (controller) {
        bool dir;
        gfx::Rect bounds = controller->CalculateMenuBounds(item, false, &dir);
        Reposition(bounds);
    }
}

void SubmenuView::Layout()
{
    // We're in a ScrollView, and need to set our width/height ourselves.
    if (!parent())
        return;

    // Use our current y, unless it means part of the menu isn't visible anymore.
    int pref_height = GetPreferredSize().height();
    int new_y;
    if (pref_height > parent()->height())
        new_y = std::max(parent()->height() - pref_height, y());
    else
        new_y = 0;
    SetBounds(x(), new_y, parent()->width(), pref_height);

    gfx::Insets insets = GetInsets();
    int x = insets.left();
    int y = insets.top();
    int menu_item_width = width() - insets.width();
    for (int i = 0; i < child_count(); ++i) {
        View* child = child_at(i);
        if (child->visible()) {
            int child_height = child->GetHeightForWidth(menu_item_width);
            child->SetBounds(x, y, menu_item_width, child_height);
            y += child_height;
        }
    }
}

gfx::Size SubmenuView::GetPreferredSize() const
{
    if (!has_children())
        return gfx::Size();

    max_minor_text_width_ = 0;
    // The maximum width of items which contain maybe a label and multiple views.
    int max_complex_width = 0;
    // The max. width of items which contain a label and maybe an accelerator.
    int max_simple_width = 0;

    // We perform the size calculation in two passes. In the first pass, we
    // calculate the width of the menu. In the second, we calculate the height
    // using that width. This allows views that have flexible widths to adjust
    // accordingly.
    for (int i = 0; i < child_count(); ++i) {
        const View* child = child_at(i);
        if (!child->visible())
            continue;
        if (child->id() == MenuItemView::kMenuItemViewID) {
            const MenuItemView* menu = static_cast<const MenuItemView*>(child);
            const MenuItemView::MenuItemDimensions& dimensions = menu->GetDimensions();
            max_simple_width = std::max(
                max_simple_width, dimensions.standard_width);
            max_minor_text_width_ = std::max(max_minor_text_width_, dimensions.minor_text_width);
            max_complex_width = std::max(max_complex_width,
                dimensions.standard_width + dimensions.children_width);
        } else {
            max_complex_width = std::max(max_complex_width,
                child->GetPreferredSize().width());
        }
    }
    if (max_minor_text_width_ > 0)
        max_minor_text_width_ += MenuConfig::instance().label_to_minor_text_padding;

    // Finish calculating our optimum width.
    gfx::Insets insets = GetInsets();
    int width = std::max(max_complex_width,
        std::max(max_simple_width + max_minor_text_width_ + insets.width(),
            minimum_preferred_width_ - 2 * insets.width()));

    // Then, the height for that width.
    int height = 0;
    int menu_item_width = width - insets.width();
    for (int i = 0; i < child_count(); ++i) {
        const View* child = child_at(i);
        height += child->visible() ? child->GetHeightForWidth(menu_item_width) : 0;
    }

    return gfx::Size(width, height + insets.height());
}

void SubmenuView::GetAccessibleState(ui::AXViewState* state)
{
    // Inherit most of the state from the parent menu item, except the role.
    if (GetMenuItem())
        GetMenuItem()->GetAccessibleState(state);
    state->role = ui::AX_ROLE_MENU_LIST_POPUP;
}

void SubmenuView::PaintChildren(const ui::PaintContext& context)
{
    View::PaintChildren(context);

    bool paint_drop_indicator = false;
    if (drop_item_) {
        switch (drop_position_) {
        case MenuDelegate::DROP_NONE:
        case MenuDelegate::DROP_ON:
            break;
        case MenuDelegate::DROP_UNKNOWN:
        case MenuDelegate::DROP_BEFORE:
        case MenuDelegate::DROP_AFTER:
            paint_drop_indicator = true;
            break;
        }
    }

    if (paint_drop_indicator) {
        gfx::Rect bounds = CalculateDropIndicatorBounds(drop_item_, drop_position_);
        ui::PaintRecorder recorder(context, size());
        recorder.canvas()->FillRect(bounds, kDropIndicatorColor);
    }
}

bool SubmenuView::GetDropFormats(
    int* formats,
    std::set<ui::Clipboard::FormatType>* format_types)
{
    DCHECK(GetMenuItem()->GetMenuController());
    return GetMenuItem()->GetMenuController()->GetDropFormats(this, formats,
        format_types);
}

bool SubmenuView::AreDropTypesRequired()
{
    DCHECK(GetMenuItem()->GetMenuController());
    return GetMenuItem()->GetMenuController()->AreDropTypesRequired(this);
}

bool SubmenuView::CanDrop(const OSExchangeData& data)
{
    DCHECK(GetMenuItem()->GetMenuController());
    return GetMenuItem()->GetMenuController()->CanDrop(this, data);
}

void SubmenuView::OnDragEntered(const ui::DropTargetEvent& event)
{
    DCHECK(GetMenuItem()->GetMenuController());
    GetMenuItem()->GetMenuController()->OnDragEntered(this, event);
}

int SubmenuView::OnDragUpdated(const ui::DropTargetEvent& event)
{
    DCHECK(GetMenuItem()->GetMenuController());
    return GetMenuItem()->GetMenuController()->OnDragUpdated(this, event);
}

void SubmenuView::OnDragExited()
{
    DCHECK(GetMenuItem()->GetMenuController());
    GetMenuItem()->GetMenuController()->OnDragExited(this);
}

int SubmenuView::OnPerformDrop(const ui::DropTargetEvent& event)
{
    DCHECK(GetMenuItem()->GetMenuController());
    return GetMenuItem()->GetMenuController()->OnPerformDrop(this, event);
}

bool SubmenuView::OnMouseWheel(const ui::MouseWheelEvent& e)
{
    gfx::Rect vis_bounds = GetVisibleBounds();
    int menu_item_count = GetMenuItemCount();
    if (vis_bounds.height() == height() || !menu_item_count) {
        // All menu items are visible, nothing to scroll.
        return true;
    }

    // Find the index of the first menu item whose y-coordinate is >= visible
    // y-coordinate.
    int i = 0;
    while ((i < menu_item_count) && (GetMenuItemAt(i)->y() < vis_bounds.y()))
        ++i;
    if (i == menu_item_count)
        return true;
    int first_vis_index = std::max(0,
        (GetMenuItemAt(i)->y() == vis_bounds.y()) ? i : i - 1);

    // If the first item isn't entirely visible, make it visible, otherwise make
    // the next/previous one entirely visible. If enough wasn't scrolled to show
    // any new rows, then just scroll the amount so that smooth scrolling using
    // the trackpad is possible.
    int delta = abs(e.y_offset() / ui::MouseWheelEvent::kWheelDelta);
    if (delta == 0)
        return OnScroll(0, e.y_offset());
    for (bool scroll_up = (e.y_offset() > 0); delta != 0; --delta) {
        int scroll_target;
        if (scroll_up) {
            if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y()) {
                if (first_vis_index == 0)
                    break;
                first_vis_index--;
            }
            scroll_target = GetMenuItemAt(first_vis_index)->y();
        } else {
            if (first_vis_index + 1 == menu_item_count)
                break;
            scroll_target = GetMenuItemAt(first_vis_index + 1)->y();
            if (GetMenuItemAt(first_vis_index)->y() == vis_bounds.y())
                first_vis_index++;
        }
        ScrollRectToVisible(gfx::Rect(gfx::Point(0, scroll_target),
            vis_bounds.size()));
        vis_bounds = GetVisibleBounds();
    }

    return true;
}

void SubmenuView::OnGestureEvent(ui::GestureEvent* event)
{
    bool handled = true;
    switch (event->type()) {
    case ui::ET_GESTURE_SCROLL_BEGIN:
        scroll_animator_->Stop();
        break;
    case ui::ET_GESTURE_SCROLL_UPDATE:
        handled = OnScroll(0, event->details().scroll_y());
        break;
    case ui::ET_GESTURE_SCROLL_END:
        break;
    case ui::ET_SCROLL_FLING_START:
        if (event->details().velocity_y() != 0.0f)
            scroll_animator_->Start(0, event->details().velocity_y());
        break;
    case ui::ET_GESTURE_TAP_DOWN:
    case ui::ET_SCROLL_FLING_CANCEL:
        if (scroll_animator_->is_scrolling())
            scroll_animator_->Stop();
        else
            handled = false;
        break;
    default:
        handled = false;
        break;
    }
    if (handled)
        event->SetHandled();
}

int SubmenuView::GetRowCount()
{
    return GetMenuItemCount();
}

int SubmenuView::GetSelectedRow()
{
    int row = 0;
    for (int i = 0; i < child_count(); ++i) {
        if (child_at(i)->id() != MenuItemView::kMenuItemViewID)
            continue;

        if (static_cast<MenuItemView*>(child_at(i))->IsSelected())
            return row;

        row++;
    }

    return -1;
}

void SubmenuView::SetSelectedRow(int row)
{
    GetMenuItem()->GetMenuController()->SetSelection(
        GetMenuItemAt(row),
        MenuController::SELECTION_DEFAULT);
}

base::string16 SubmenuView::GetTextForRow(int row)
{
    return GetMenuItemAt(row)->title();
}

bool SubmenuView::IsShowing()
{
    return host_ && host_->IsMenuHostVisible();
}

void SubmenuView::ShowAt(Widget* parent,
    const gfx::Rect& bounds,
    bool do_capture)
{
    if (host_) {
        host_->ShowMenuHost(do_capture);
    } else {
        host_ = new MenuHost(this);
        // Force construction of the scroll view container.
        GetScrollViewContainer();
        // Force a layout since our preferred size may not have changed but our
        // content may have.
        InvalidateLayout();
        host_->InitMenuHost(parent, bounds, scroll_view_container_, do_capture);
    }

    GetScrollViewContainer()->NotifyAccessibilityEvent(
        ui::AX_EVENT_MENU_START,
        true);
    NotifyAccessibilityEvent(
        ui::AX_EVENT_MENU_POPUP_START,
        true);
}

void SubmenuView::Reposition(const gfx::Rect& bounds)
{
    if (host_)
        host_->SetMenuHostBounds(bounds);
}

void SubmenuView::Close()
{
    if (host_) {
        NotifyAccessibilityEvent(ui::AX_EVENT_MENU_POPUP_END, true);
        GetScrollViewContainer()->NotifyAccessibilityEvent(
            ui::AX_EVENT_MENU_END, true);

        host_->DestroyMenuHost();
        host_ = NULL;
    }
}

void SubmenuView::Hide()
{
    if (host_)
        host_->HideMenuHost();
    if (scroll_animator_->is_scrolling())
        scroll_animator_->Stop();
}

void SubmenuView::ReleaseCapture()
{
    if (host_)
        host_->ReleaseMenuHostCapture();
}

bool SubmenuView::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e)
{
    return views::FocusManager::IsTabTraversalKeyEvent(e);
}

MenuItemView* SubmenuView::GetMenuItem() const
{
    return parent_menu_item_;
}

void SubmenuView::SetDropMenuItem(MenuItemView* item,
    MenuDelegate::DropPosition position)
{
    if (drop_item_ == item && drop_position_ == position)
        return;
    SchedulePaintForDropIndicator(drop_item_, drop_position_);
    drop_item_ = item;
    drop_position_ = position;
    SchedulePaintForDropIndicator(drop_item_, drop_position_);
}

bool SubmenuView::GetShowSelection(MenuItemView* item)
{
    if (drop_item_ == NULL)
        return true;
    // Something is being dropped on one of this menus items. Show the
    // selection if the drop is on the passed in item and the drop position is
    // ON.
    return (drop_item_ == item && drop_position_ == MenuDelegate::DROP_ON);
}

MenuScrollViewContainer* SubmenuView::GetScrollViewContainer()
{
    if (!scroll_view_container_) {
        scroll_view_container_ = new MenuScrollViewContainer(this);
        // Otherwise MenuHost would delete us.
        scroll_view_container_->set_owned_by_client();
    }
    return scroll_view_container_;
}

void SubmenuView::MenuHostDestroyed()
{
    host_ = NULL;
    GetMenuItem()->GetMenuController()->Cancel(MenuController::EXIT_DESTROYED);
}

const char* SubmenuView::GetClassName() const
{
    return kViewClassName;
}

void SubmenuView::OnBoundsChanged(const gfx::Rect& previous_bounds)
{
    SchedulePaint();
}

void SubmenuView::SchedulePaintForDropIndicator(
    MenuItemView* item,
    MenuDelegate::DropPosition position)
{
    if (item == NULL)
        return;

    if (position == MenuDelegate::DROP_ON) {
        item->SchedulePaint();
    } else if (position != MenuDelegate::DROP_NONE) {
        SchedulePaintInRect(CalculateDropIndicatorBounds(item, position));
    }
}

gfx::Rect SubmenuView::CalculateDropIndicatorBounds(
    MenuItemView* item,
    MenuDelegate::DropPosition position)
{
    DCHECK(position != MenuDelegate::DROP_NONE);
    gfx::Rect item_bounds = item->bounds();
    switch (position) {
    case MenuDelegate::DROP_BEFORE:
        item_bounds.Offset(0, -kDropIndicatorHeight / 2);
        item_bounds.set_height(kDropIndicatorHeight);
        return item_bounds;

    case MenuDelegate::DROP_AFTER:
        item_bounds.Offset(0, item_bounds.height() - kDropIndicatorHeight / 2);
        item_bounds.set_height(kDropIndicatorHeight);
        return item_bounds;

    default:
        // Don't render anything for on.
        return gfx::Rect();
    }
}

bool SubmenuView::OnScroll(float dx, float dy)
{
    const gfx::Rect& vis_bounds = GetVisibleBounds();
    const gfx::Rect& full_bounds = bounds();
    int x = vis_bounds.x();
    float y_f = vis_bounds.y() - dy - roundoff_error_;
    int y = gfx::ToRoundedInt(y_f);
    roundoff_error_ = y - y_f;
    // clamp y to [0, full_height - vis_height)
    y = std::min(y, full_bounds.height() - vis_bounds.height() - 1);
    y = std::max(y, 0);
    gfx::Rect new_vis_bounds(x, y, vis_bounds.width(), vis_bounds.height());
    if (new_vis_bounds != vis_bounds) {
        ScrollRectToVisible(new_vis_bounds);
        return true;
    }
    return false;
}

} // namespace views
